package ai.platon.pulsar.common.io

data class KeyDefinition(
    /**
     * The value of the key pressed by the user, taking into consideration the state of modifier keys such as Shift
     * as well as the keyboard locale and layout.
     *
     * Examples: "a", "b", "A", "B", "1", "2", "3", "Shift", "Enter", ...
     *
     * @see [KeyboardEvent: key property](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key)
     * */
    val key: String,
    /**
     * Represents a system and implementation dependent numerical code identifying the unmodified value of the pressed key.
     *
     * Note: The key code is a numeric value that represents a physical key on the keyboard. It's not the character.
     *
     * Key code examples: 13, 65, 51, ...
     *
     * You should avoid using this if possible; it's been deprecated for some time. Instead, you should use code if
     * available. Codes are more reliable than key codes, because they are not affected by layout.
     * Code example: "KeyA", "Digit3", "Enter", ...
     *
     * @see [KeyboardEvent: keyCode property](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode)
     * */
    val keyCode: Int,
    /**
     * The key code without location, like 13, 65, 51, ...
     * */
    val keyCodeWithoutLocation: Int? = null,
    /**
     * The key name of the shifted key, like "KeyA", "KeyB", ...
     * */
    val shiftKey: String? = null,
    /**
     * The key code of the shifted key, like 65, 66, ...
     * */
    val shiftKeyCode: Int? = null,
    /**
     * Returns the value of the key pressed by the user.
     *
     * Example values: "Enter", "a", "3", ...
     *
     * @see [KeyboardEvent: key property](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key)
     * */
    val text: String? = null,
    /**
     * Returns an int representing the location of the key on the keyboard.
     * Example values: 0, 1, 2, 3, ...
     *
     * @see [KeyboardEvent: location property](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/location)
     * */
    val location: Int? = null
)

/**
 * A virtual key represents a physical key on the keyboard. It's not the character generated by pressing the key.
 *
 * Since the input device isn't a physical keyboard, but is instead a virtual keyboard, the virtual key will be set by
 * the browser to match as closely as possible to what would happen with a physical keyboard, to maximize compatibility
 * between physical and virtual input devices.
 *
 * @see [Code values for keyboard events](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values)
 * @see [KeyboardEvent: code property](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code)
 * @see [KeyboardEvent: key property](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key)
 * @see [KeyboardEvent: location property](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/location)
 * */
data class VirtualKey(
    /**
     * The [VirtualKey.key] of the key pressed by the user, taking into consideration the state of modifier keys such as Shift
     * as well as the keyboard locale and layout.
     *
     * Examples: "a", "b", "A", "B", "1", "2", "3", "Shift", "Enter", ...
     *
     * @see [KeyboardEvent: key property](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key)
     * */
    var key: String,
    /**
     * Represents a system and implementation dependent numerical code identifying the unmodified value of the pressed key.
     *
     * Note: The key code is a numeric value that represents a physical key on the keyboard. It's not the character.
     *
     * Key code examples: 13, 65, 51, ...
     *
     * You should avoid using this if possible; it's been deprecated for some time. Instead, you should use code if
     * available. Codes are more reliable than key codes, because they are not affected by layout.
     * Code example: "KeyA", "Digit3", "Enter", ...
     *
     * @see [KeyboardEvent: keyCode property](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode)
     * */
    var keyCodeWithoutLocation: Int,
    /**
     * The [VirtualKey.code] property represents a physical key on the keyboard (as opposed to the character generated by
     * pressing the key). In other words, this property returns a value that isn't altered by keyboard layout or the
     * state of the modifier keys.
     *
     * If the input device isn't a physical keyboard, but is instead a virtual keyboard or accessibility device, the
     * returned value will be set by the browser to match as closely as possible to what would happen with a physical
     * keyboard, to maximize compatibility between physical and virtual input devices.
     *
     * For example, the code returned is "KeyQ" for the Q key on a QWERTY layout keyboard.
     * Code example: "KeyA", "Digit3", "Enter", ...
     *
     * @see [KeyboardEvent: code property](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code)
     * @see [Code values for keyboard events](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values)
     * */
    var code: String,
    /**
     * The [VirtualKey.location] read-only property returns an unsigned long representing the location of the key on
     * the keyboard or other input device.
     * */
    var location: Int = 0,
    /**
     * The deprecated [VirtualKey.keyCode] property represents a system and implementation dependent
     * numerical code identifying the unmodified value of the pressed key.
     *
     * Note: The key code is a numeric value that represents a physical key on the keyboard. It's not the character.
     *
     * Key code examples: 13, 65, 51, ...
     *
     * You should avoid using this if possible; it's been deprecated for some time. Instead, you should use code if
     * available. Codes are more reliable than key codes, because they are not affected by layout.
     *
     * @see [KeyboardEvent: keyCode property](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode)
     * */
    var keyCode: Int,
    /**
     * Returns the value of the key pressed by the user.
     * If the key is a printable character, this property returns the character.
     * */
    var text: String,
    /**
     * The shifted key definition.
     * */
    var shifted: VirtualKey? = null,
) {
    val isModifier: Boolean
        get() = key in VirtualKeyboard.KEYBOARD_MODIFIERS
}

typealias KeyboardLayout = Map<String, KeyDefinition>

enum class KeyboardModifier {
    Alt, Control, Meta, Shift;

    companion object {
        fun valueOfOrNull(s: String?): KeyboardModifier? {
            val ls = s?.lowercase() ?: return null
            return when(ls) {
                "alt" -> return Alt
                "control", "ctrl" -> return Control
                "meta" -> return Meta
                "shift" -> return Shift
                else -> null
            }
        }
    }
}

val USKeypadLocation = 3

/**
 * The US keyboard layout.
 * */
val USKeyboardLayout: KeyboardLayout = mapOf(
    // Functions row
    "Escape" to KeyDefinition("Escape", 27),
    "F1" to KeyDefinition("F1", 112),
    "F2" to KeyDefinition("F2", 113),
    "F3" to KeyDefinition("F3", 114),
    "F4" to KeyDefinition("F4", 115),
    "F5" to KeyDefinition("F5", 116),
    "F6" to KeyDefinition("F6", 117),
    "F7" to KeyDefinition("F7", 118),
    "F8" to KeyDefinition("F8", 119),
    "F9" to KeyDefinition("F9", 120),
    "F10" to KeyDefinition("F10", 121),
    "F11" to KeyDefinition("F11", 122),
    "F12" to KeyDefinition("F12", 123),

    // Numbers row
    "Backquote" to KeyDefinition("`", 192, shiftKey = "~"),
    "Digit1" to KeyDefinition("1", 49, shiftKey = "!"),
    "Digit2" to KeyDefinition("2", 50, shiftKey = "@"),
    "Digit3" to KeyDefinition("3", 51, shiftKey = "#"),
    "Digit4" to KeyDefinition("4", 52, shiftKey = "$"),
    "Digit5" to KeyDefinition("5", 53, shiftKey = "%"),
    "Digit6" to KeyDefinition("6", 54, shiftKey = "^"),
    "Digit7" to KeyDefinition("7", 55, shiftKey = "&"),
    "Digit8" to KeyDefinition("8", 56, shiftKey = "*"),
    "Digit9" to KeyDefinition("9", 57, shiftKey = "("),
    "Digit0" to KeyDefinition("0", 48, shiftKey = ")"),
    "Minus" to KeyDefinition("-", 189, shiftKey = "_"),
    "Equal" to KeyDefinition("=", 187, shiftKey = "+"),
    "Backslash" to KeyDefinition("\\", 220, shiftKey = "|"),
    "Backspace" to KeyDefinition("Backspace", 8),

    // First row
    "Tab" to KeyDefinition("Tab", 9),
    "KeyQ" to KeyDefinition("q", 81, shiftKey = "Q"),
    "KeyW" to KeyDefinition("w", 87, shiftKey = "W"),
    "KeyE" to KeyDefinition("e", 69, shiftKey = "E"),
    "KeyR" to KeyDefinition("r", 82, shiftKey = "R"),
    "KeyT" to KeyDefinition("t", 84, shiftKey = "T"),
    "KeyY" to KeyDefinition("y", 89, shiftKey = "Y"),
    "KeyU" to KeyDefinition("u", 85, shiftKey = "U"),
    "KeyI" to KeyDefinition("i", 73, shiftKey = "I"),
    "KeyO" to KeyDefinition("o", 79, shiftKey = "O"),
    "KeyP" to KeyDefinition("p", 80, shiftKey = "P"),
    "BracketLeft" to KeyDefinition("[", 219, shiftKey = "{"),
    "BracketRight" to KeyDefinition("]", 221, shiftKey = "}"),

    // Second row
    "CapsLock" to KeyDefinition("CapsLock", 20),
    "KeyA" to KeyDefinition("a", 65, shiftKey = "A"),
    "KeyS" to KeyDefinition("s", 83, shiftKey = "S"),
    "KeyD" to KeyDefinition("d", 68, shiftKey = "D"),
    "KeyF" to KeyDefinition("f", 70, shiftKey = "F"),
    "KeyG" to KeyDefinition("g", 71, shiftKey = "G"),
    "KeyH" to KeyDefinition("h", 72, shiftKey = "H"),
    "KeyJ" to KeyDefinition("j", 74, shiftKey = "J"),
    "KeyK" to KeyDefinition("k", 75, shiftKey = "K"),
    "KeyL" to KeyDefinition("l", 76, shiftKey = "L"),
    "Semicolon" to KeyDefinition(";", 186, shiftKey = ":"),
    "Quote" to KeyDefinition("'", 222, shiftKey = "\""),
    "Enter" to KeyDefinition("Enter", 13, text = "\r"),

    // Third row
    "ShiftLeft" to KeyDefinition("Shift", 160, keyCodeWithoutLocation = 16, location = 1),
    "KeyZ" to KeyDefinition("z", 90, shiftKey = "Z"),
    "KeyX" to KeyDefinition("x", 88, shiftKey = "X"),
    "KeyC" to KeyDefinition("c", 67, shiftKey = "C"),
    "KeyV" to KeyDefinition("v", 86, shiftKey = "V"),
    "KeyB" to KeyDefinition("b", 66, shiftKey = "B"),
    "KeyN" to KeyDefinition("n", 78, shiftKey = "N"),
    "KeyM" to KeyDefinition("m", 77, shiftKey = "M"),
    "Comma" to KeyDefinition(",", 188, shiftKey = "<"),
    "Period" to KeyDefinition(".", 190, shiftKey = ">"),
    "Slash" to KeyDefinition("/", 191, shiftKey = "?"),
    "ShiftRight" to KeyDefinition("Shift", 161, keyCodeWithoutLocation = 16, location = 2),

    // Last row
    "ControlLeft" to KeyDefinition("Control", 162, keyCodeWithoutLocation = 17, location = 1),
    "MetaLeft" to KeyDefinition("Meta", 91, location = 1),
    "AltLeft" to KeyDefinition("Alt", 164, keyCodeWithoutLocation = 18, location = 1),
    "Space" to KeyDefinition(" ", 32),
    "AltRight" to KeyDefinition("Alt", 165, keyCodeWithoutLocation = 18, location = 2),
    "AltGraph" to KeyDefinition("AltGraph", 225),
    "MetaRight" to KeyDefinition("Meta", 92, location = 2),
    "ContextMenu" to KeyDefinition("ContextMenu", 93),
    "ControlRight" to KeyDefinition("Control", 163, keyCodeWithoutLocation = 17, location = 2),

    // Center block
    "PrintScreen" to KeyDefinition("PrintScreen", 44),
    "ScrollLock" to KeyDefinition("ScrollLock", 145),
    "Pause" to KeyDefinition("Pause", 19),

    "PageUp" to KeyDefinition("PageUp", 33),
    "PageDown" to KeyDefinition("PageDown", 34),
    "Insert" to KeyDefinition("Insert", 45),
    "Delete" to KeyDefinition("Delete", 46),
    "Home" to KeyDefinition("Home", 36),
    "End" to KeyDefinition("End", 35),

    // Arrow keys
    "ArrowLeft" to KeyDefinition("ArrowLeft", 37),
    "ArrowUp" to KeyDefinition("ArrowUp", 38),
    "ArrowRight" to KeyDefinition("ArrowRight", 39),
    "ArrowDown" to KeyDefinition("ArrowDown", 40),

    // Numpad
    "NumLock" to KeyDefinition("NumLock", 144),
    "NumpadDivide" to KeyDefinition("/", 111, location = 3),
    "NumpadMultiply" to KeyDefinition("*", 106, location = 3),
    "NumpadSubtract" to KeyDefinition("-", 109, location = 3),
    "NumpadAdd" to KeyDefinition("+", 107, location = 3),
    "Numpad1" to KeyDefinition("1", 35, shiftKeyCode = 97, location = 3),
    "Numpad2" to KeyDefinition("2", 40, shiftKeyCode = 98, location = 3),
    "Numpad3" to KeyDefinition("3", 34, shiftKeyCode = 99, location = 3),
    "Numpad4" to KeyDefinition("4", 37, shiftKeyCode = 100, location = 3),
    "Numpad5" to KeyDefinition("5", 12, shiftKeyCode = 101, location = 3),
    "Numpad6" to KeyDefinition("6", 39, shiftKeyCode = 102, location = 3),
    "Numpad7" to KeyDefinition("7", 36, shiftKeyCode = 103, location = 3),
    "Numpad8" to KeyDefinition("8", 38, shiftKeyCode = 104, location = 3),
    "Numpad9" to KeyDefinition("9", 33, shiftKeyCode = 105, location = 3),
    "Numpad0" to KeyDefinition("0", 45, shiftKeyCode = 96, location = 3),
    "NumpadDecimal" to KeyDefinition(".", 46, shiftKeyCode = 110, location = 3),
    "NumpadEnter" to KeyDefinition("Enter", 13, text = "\r", location = 3)
)

object VirtualKeyboard {

    val CODE_ALIASES = mapOf(
        "ShiftLeft" to listOf("Shift"),
        "ControlLeft" to listOf("Control"),
        "AltLeft" to listOf("Alt"),
        "MetaLeft" to listOf("Meta"),
        "Enter" to listOf("\n", "\r")
    )

    val KEYBOARD_MODIFIERS = KeyboardModifier.entries.map { it.name }

    val KEYPAD_LOCATION = USKeypadLocation

    /**
     * Maps from key codes or key texts to key descriptions.
     *
     * The keys of the map like:
     * Code: "Digit3", "KeyA", "Enter", ...
     * Key: "3", "a", "\r", ...
     * Code aliases: "\n", "ShiftLeft", ...
     *
     * @see [KeyboardEvent: code property](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code)
     *
     * */
    val KEYBOARD_LAYOUT = buildExtendedKeyboardLayoutMapping(USKeyboardLayout)

    private fun buildExtendedKeyboardLayoutMapping(layout: KeyboardLayout): Map<String, VirtualKey> {
        val result = mutableMapOf<String, VirtualKey>()
        // The key code: KeyA, KeyB, Enter, ...
        for ((code, definition) in layout) {
            val virtualKey = VirtualKey(
                key = definition.key, // default ""
                keyCode = definition.keyCode, // default 0
                keyCodeWithoutLocation = definition.keyCodeWithoutLocation ?: definition.keyCode, // default 0
                code = code,
                text = definition.text ?: "", // default ""
                location = definition.location ?: 0 // default 0
            )
            if (definition.key.length == 1) {
                virtualKey.text = virtualKey.key
            }

            val shiftKey = definition.shiftKey
            val shiftedDescription = if (shiftKey != null) {
                require(shiftKey.length == 1)
                virtualKey.copy(
                    key = shiftKey,
                    text = shiftKey,
                    keyCode = definition.shiftKeyCode ?: definition.keyCode,
                    shifted = null
                )
            } else null

            // Map from code: Digit3 -> { ... description, shifted }
            result[code] = virtualKey.copy(shifted = shiftedDescription)

            // Map from aliases: Shift -> non-shift definition
            CODE_ALIASES[code]?.forEach { alias ->
                result[alias] = virtualKey
            }

            // Do not use numpad when converting keys to codes.
            if (definition.location != null) {
                continue
            }

            // Map from key, no shifted
            if (virtualKey.key.length == 1) {
                result[virtualKey.key] = virtualKey
            }

            // Map from shiftKey, no shifted
            shiftedDescription?.let { shifted ->
                result[shifted.key] = shifted.copy(shifted = null)
            }
        }

        return result
    }
}
